package com.unknow.first.util;


import static com.unknow.first.mfa.config.MfaFilterConfig.__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY;
import static org.cloud.constant.MfaConstant.CORRELATION_YOUR_GOOGLE_KEY;
import static org.cloud.constant.MfaConstant.MFA_HEADER_NAME;
import static org.cloud.constant.MfaConstant._GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME;
import static org.cloud.constant.MfaConstant._GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME;

import cn.hutool.core.util.ObjectUtil;
import com.unknow.first.mfa.service.GoogleAuthenticatorService;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
import org.cloud.context.RequestContext;
import org.cloud.context.RequestContextManager;
import org.cloud.core.redis.RedisUtil;
import org.cloud.encdec.service.AESService;
import org.cloud.entity.LoginUserDetails;
import org.cloud.exception.BusinessException;
import org.cloud.feign.service.ICommonServiceFeignClient;
import org.cloud.utils.CollectionUtil;
import org.cloud.utils.HttpServletUtil;
import org.cloud.utils.SpringContextUtil;
import org.cloud.vo.FrameUserRefVO;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;

/**
 * google身份验证器，java服务端实现
 *
 * @author yangbo
 * @version 创建时间：2017年8月14日 上午10:10:02
 */
@Slf4j
public final class GoogleAuthenticatorUtil {

    // 生成的key长度( Generate secret key length)
    public final int SECRET_SIZE = 15;
    public final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
    // Java实现随机数算法
    public final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    /**
     * Generate a random secret key. This must be saved by the server and associated with the users account to verify the code displayed by
     * Google Authenticator. The user must register this secret on their device. 生成一个随机秘钥
     *
     * @return secret key
     */
    public String generateSecretKey() {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(SEED));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String encodedKey = new String(bEncodedKey);
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            // should never occur... configuration error
        }
        return null;
    }

    /**
     * Return a URL that generates and displays a QR barcode. The user scans this bar code with the Google Authenticator application on
     * their smartphone to register the auth code. They can also manually enter the secret if desired
     *
     * @param user   user id (e.g. fflinstone)
     * @param host   host or system that the code is for (e.g. myapp.com)
     * @param secret the secret that was previously generated for this user
     * @return the URL for the QR code to scan
     */
    @SneakyThrows
    public String getQRBarcodeURL(String user, String host, String secret) {
        final String otpauth = "otpauth://totp/%s@%s?secret=%s";
        final String barCodeApiUrl = "https://api.pwmqr.com/qrcode/create/?url=";
        return barCodeApiUrl + URLEncoder.encode(String.format(otpauth, user, host, secret), "utf8");
    }

    /**
     * 生成一个google身份验证器，识别的字符串，只需要把该方法返回值生成二维码扫描就可以了。
     *
     * @param user   账号
     * @param secret 密钥
     * @return
     */
    public String getQRBarcode(String user, String secret) {
        String format = "otpauth://totp/%s?secret=%s";
        return String.format(format, user, secret);
    }

    /**
     * Check the code entered by the user to see if it is valid 验证code是否合法
     *
     * @param secret   The users secret.
     * @param code     The code displayed on the users device
     * @param timeMsec The time in msec (System.currentTimeMillis() for example)
     * @return
     */
    public boolean checkCode(String secret, long code, long timeMsec) {
        return authenticatorService.checkCode(secret, code, timeMsec);
    }

    /**
     * @return
     * @throws Exception
     */
    public String getCurrentUserVerifyKey() throws Exception {

        String result = this.getCurrentUserVerifyKey(false);
        if (result != null) {
            return result;
        }
        RequestContext currentRequestContext = RequestContextManager.single().getRequestContext();
        LoginUserDetails user = currentRequestContext.getUser();
        FrameUserRefVO googleSecretRefVO = this.createNewUserRefVO(user);
        commonServiceFeignClient.addUserRef(googleSecretRefVO);
        final Map<String, String> exceptionObject = new LinkedHashMap<>();
        exceptionObject.put("description", CORRELATION_YOUR_GOOGLE_KEY.description());
        exceptionObject.put("secret", googleSecretRefVO.getAttributeValue());
        exceptionObject.put("secretQRBarcode", this.getQRBarcode(user.getUsername(), googleSecretRefVO.getAttributeValue()));
        exceptionObject.put("secretQRBarcodeURL", this.getQRBarcodeURL(user.getUsername(), "", googleSecretRefVO.getAttributeValue()));
        RedisUtil.single().set(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId(), googleSecretRefVO.getAttributeValue(), -1L);
        throw new BusinessException(CORRELATION_YOUR_GOOGLE_KEY.value(), exceptionObject, HttpStatus.BAD_REQUEST.value()); //
    }

    /**
     * @return
     * @throws Exception
     */
    public String getCurrentUserVerifyKey(Boolean isRefresh) throws Exception {
        RequestContext currentRequestContext = RequestContextManager.single().getRequestContext();
        LoginUserDetails user = currentRequestContext.getUser();
        if (!isRefresh) {
            String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId());
            if (CollectionUtil.single().isNotEmpty(googleSecret)) {
                return googleSecret;
            }
        }
        FrameUserRefVO googleSecretRefVO = commonServiceFeignClient.getCurrentUserRefByAttributeName(
            _GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.value());
        //  如果未绑定谷歌验证那么插入谷歌验证属性
        if (ObjectUtil.isNotEmpty(googleSecretRefVO)) {
            if (isRefresh) {
                googleSecretRefVO.setAttributeValue(aesService.encrypt(this.generateSecretKey()));
                commonServiceFeignClient.updateUserRef(googleSecretRefVO);
            }
            RedisUtil.single().set(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId(), googleSecretRefVO.getAttributeValue(), -1L);
            return googleSecretRefVO.getAttributeValue();
        }
        return null;

    }

    /**
     * 校验当前用户是否已经绑定谷歌验证码
     */
    public void verifyCurrentUserBindGoogleKey() throws BusinessException {
        FrameUserRefVO frameUserRefVO = commonServiceFeignClient.getCurrentUserRefByAttributeName(
            _GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value());

        if (frameUserRefVO == null || "false".equals(frameUserRefVO.getAttributeValue())) {
            throw new BusinessException(CORRELATION_YOUR_GOOGLE_KEY.value(), CORRELATION_YOUR_GOOGLE_KEY.description(),
                HttpStatus.BAD_REQUEST.value());
        }
    }

    public Boolean checkGoogleVerifyCode(String googleSecret) throws BusinessException {
        final String mfaValue = HttpServletUtil.single().getHttpServlet().getHeader(MFA_HEADER_NAME.value());
        return checkGoogleVerifyCode(googleSecret, mfaValue);
    }

    public Boolean checkGoogleVerifyCode(final String googleSecretEnc, final String mfaValue) throws BusinessException {
        return authenticatorService.checkGoogleVerifyCode(googleSecretEnc, mfaValue);
    }

    @SneakyThrows
    public FrameUserRefVO createNewUserRefVO(LoginUserDetails loginUserDetails) {
        final String googleSecret = this.generateSecretKey();
        FrameUserRefVO frameUserRefVO = new FrameUserRefVO();
        frameUserRefVO.setAttributeName(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.value());
        frameUserRefVO.setUserId(loginUserDetails.getId());
        frameUserRefVO.setAttributeValue(aesService.encrypt(googleSecret));
        frameUserRefVO.setRemark(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.description());
        frameUserRefVO.setCreateBy(loginUserDetails.getUsername());
        frameUserRefVO.setUpdateBy(loginUserDetails.getUsername());
        frameUserRefVO.setRemark(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.description());
        return frameUserRefVO;
    }

    public void checkGoogleValidCode() throws Exception {
        verifyCurrentUserBindGoogleKey();
        String googleSecret = getCurrentUserVerifyKey();
        if (!GoogleAuthenticatorUtil.single().checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }

    public void checkGoogleValidCode(Long userId) throws Exception {
        String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + userId);
        if (!checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }

    public void checkGoogleValidCode(Long userId, String mfaValue) throws Exception {
        String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + userId);
        if (!checkGoogleVerifyCode(googleSecret, mfaValue)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }


    private final ICommonServiceFeignClient commonServiceFeignClient;
    private final GoogleAuthenticatorService authenticatorService;
    private final AESService aesService;

    @Lazy
    private GoogleAuthenticatorUtil() {
        commonServiceFeignClient = SpringContextUtil.getBean(ICommonServiceFeignClient.class);
        authenticatorService = SpringContextUtil.getBean(GoogleAuthenticatorService.class);
        aesService = SpringContextUtil.getBean(AESService.class);
    }

    private final static GoogleAuthenticatorUtil INSTANCE = new GoogleAuthenticatorUtil();

    public static GoogleAuthenticatorUtil single() {
        return INSTANCE;
    }

}
